/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sentry;

import io.sentry.protocol.DebugMeta;
import io.sentry.protocol.Message;
import io.sentry.protocol.SentryException;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.SentryThread;
import io.sentry.util.CollectionUtils;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

/**
 * SentryEvent
 *
 * @since 2021-06-02
 */
public final class SentryEvent extends SentryBaseEvent implements IUnknownPropertiesConsumer {
    /**
     * Timestamp when the event was created.
     *
     * <p>Indicates when the event was created in the Sentry SDK. The format is either a string as
     * defined in [RFC 3339](https://tools.ietf.org/html/rfc3339) or a numeric (integer or float)
     * value representing the number of seconds that have elapsed since the [Unix
     * epoch](https://en.wikipedia.org/wiki/Unix_time).
     *
     * <p>Sub-microsecond precision is not preserved with numeric values due to precision limitations
     * with floats (at least in our systems). With that caveat in mind, just send whatever is easiest
     * to produce.
     *
     * <p>All timestamps in the event protocol are formatted this way.
     *
     * <p>```json { "timestamp": "2011-05-02T17:41:36Z" } { "timestamp": 1304358096.0 } ```
     */
    private final @NotNull Date timestamp;

    private @Nullable Message message;

    /**
     * Logger that created the event.
     */
    private @Nullable String logger;
    /**
     * Threads that were active when the event occurred.
     */
    private @Nullable SentryValues<SentryThread> threads;
    /**
     * One or multiple chained (nested) exceptions.
     */
    private @Nullable SentryValues<SentryException> exception;
    /**
     * Severity level of the event. Defaults to `error`.
     *
     * <p>Example:
     *
     * <p>```json {"level": "warning"} ```
     */
    private @Nullable SentryLevel level;
    /**
     * Transaction name of the event.
     *
     * For example, in a web app, this might be the route name ("/users/username/"or
     * UserView), in a task queue it might be the function + module name.
     */
    private @Nullable String transaction;

    /**
     * Manual fingerprint override.
     *
     * <p>A list of strings used to dictate how this event is supposed to be grouped with other events
     * into issues. For more information about overriding grouping see [Customize Grouping with
     * Fingerprints](https://docs.sentry.io/data-management/event-grouping/).
     *
     * <p>```json { "fingerprint": ["myrpc", "POST", "/foo.bar"] }
     */
    private @Nullable List<String> fingerprint;

    private @Nullable Map<String, Object> unknown;
    /**
     * Name and versions of all installed modules/packages/dependencies in the current
     * environment/application.
     *
     * <p>```json { "django": "3.0.0", "celery": "4.2.1" } ```
     *
     * <p>In Python this is a list of installed packages as reported by `pkg_resources` together with
     * their reported version string.
     *
     * <p>This is primarily used for suggesting to enable certain SDK integrations from within the UI
     * and for making informed decisions on which frameworks to support in future development efforts.
     */
    private @Nullable Map<String, String> modules;
    /**
     * Meta data for event processing and debugging.
     */
    private @Nullable DebugMeta debugMeta;

    SentryEvent(final @NotNull SentryId eventId, final @NotNull Date timestamp) {
        super(eventId);
        this.timestamp = timestamp;
    }

    /**
     * SentryEvent ctor with the captured Throwable
     *
     * @param throwable the Throwable or null
     */
    public SentryEvent(final @Nullable Throwable throwable) {
        this();
        this.throwable = throwable;
    }

    /**
     * constructor
     */
    public SentryEvent() {
        this(new SentryId(), DateUtils.getCurrentDateTime());
    }

    /**
     * constructor
     *
     * @param timestamp date
     */
    @TestOnly
    public SentryEvent(final @NotNull Date timestamp) {
        this(new SentryId(), timestamp);
    }

    @SuppressWarnings({"JdkObsolete", "JavaUtilDate"})
    public Date getTimestamp() {
        return (Date) timestamp.clone();
    }

    public @Nullable Message getMessage() {
        return message;
    }

    public void setMessage(final @Nullable Message message) {
        this.message = message;
    }

    public @Nullable String getLogger() {
        return logger;
    }

    public void setLogger(final @Nullable String logger) {
        this.logger = logger;
    }

    /**
     * get threads
     *
     * @return threads
     */
    public @Nullable List<SentryThread> getThreads() {
        if (threads != null) {
            return threads.getValues();
        } else {
            return null;
        }
    }

    public void setThreads(final @Nullable List<SentryThread> threads) {
        this.threads = new SentryValues<>(threads);
    }

    /**
     * getExceptions
     *
     * @return List
     */
    public @Nullable List<SentryException> getExceptions() {
        return exception == null ? null : exception.getValues();
    }

    /**
     * setExceptions
     *
     * @param exceptions exceptions
     */
    public void setExceptions(final @Nullable List<SentryException> exceptions) {
        this.exception = new SentryValues<>(exceptions);
    }

    public @Nullable SentryLevel getLevel() {
        return level;
    }

    public void setLevel(final @Nullable SentryLevel level) {
        this.level = level;
    }

    public @Nullable String getTransaction() {
        return transaction;
    }

    public void setTransaction(final @Nullable String transaction) {
        this.transaction = transaction;
    }

    /**
     * getFingerprints
     *
     * @return List
     */
    public @Nullable List<String> getFingerprints() {
        return fingerprint;
    }

    /**
     * setFingerprints
     *
     * @param fingerprints fingerprints
     */
    public void setFingerprints(final @Nullable List<String> fingerprints) {
        this.fingerprint = fingerprints != null ? new ArrayList<>(fingerprints) : null;
    }

    @ApiStatus.Internal
    @Override
    public void acceptUnknownProperties(final @NotNull Map<String, Object> unknowns) {
        this.unknown = unknowns;
    }

    @TestOnly
    public @Nullable Map<String, Object> getUnknown() {
        return unknown;
    }

    @Nullable
    Map<String, String> getModules() {
        return modules;
    }

    public void setModules(final @Nullable Map<String, String> modules) {
        this.modules = CollectionUtils.newHashMap(modules);
    }

    /**
     * set module
     *
     * @param key  key
     * @param value value
     */
    public void setModule(final @NotNull String key, final @NotNull String value) {
        if (modules == null) {
            modules = new HashMap<>();
        }
        modules.put(key, value);
    }

    /**
     * remove module
     *
     * @param key key
     */
    public void removeModule(final @NotNull String key) {
        if (modules != null) {
            modules.remove(key);
        }
    }

    /**
     * get module
     *
     * @param key key
     * @return module
     */
    public @Nullable String getModule(final @NotNull String key) {
        if (modules != null) {
            return modules.get(key);
        }
        return null;
    }

    public @Nullable DebugMeta getDebugMeta() {
        return debugMeta;
    }

    public void setDebugMeta(final @Nullable DebugMeta debugMeta) {
        this.debugMeta = debugMeta;
    }

    /**
     * Returns true if any exception was unhandled by the user.
     *
     * @return true if its crashed or false otherwise
     */
    public boolean isCrashed() {
        if (exception != null) {
            for (SentryException exception : exception.getValues()) {
                if (exception.getMechanism() != null
                    && exception.getMechanism().isHandled() != null
                    && !exception.getMechanism().isHandled()) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Returns true if this event has any sort of exception
     *
     * @return true if errored or false otherwise
     */
    public boolean isErrored() {
        return exception != null && !exception.getValues().isEmpty();
    }
}
